package com.pinfly.common.util;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;

import com.pinfly.common.exception.MSException;

public class LogUtil
{
    /**
     * Convenience field for getting the systems default line separator.
     */
    public static final String LS = System.getProperty ("line.separator");
    private static final int DEFAULT_STACK_DEPTH = 20;
    private static final Calendar s_cal = Calendar.getInstance ();

    /**
     * Performs a limited URLEncoding on the input String. Carriage returns, and
     * commas are encoded.
     * 
     * @param str The String to encode.
     * @return The encoded String.
     */
    public static String encode (String str)
    {
        StringBuffer buff = new StringBuffer ();
        return (encode (buff, str).toString ());

    }

    /**
     * Performs a limited URLEncoding on the input String. Carriage returns, and
     * commas are encoded.
     * 
     * @param buff The StringBuffer to place the encoded results in.
     * @param str The String to encode.
     * @return The StringBuffer that has the encoded results.
     */
    public static StringBuffer encode (StringBuffer buff, String str)
    {
        if (str == null)
        {
            return (buff);

        }

        if (str.length () == 0)
        {
            return (buff);
        }

        // Do a quick check to see if there are any characters to
        // encode
        if (str.indexOf ('\n') == -1 && str.indexOf ('\r') == -1 && str.indexOf (',') == -1)
        {
            buff.append (str);
            return (buff);

        }

        char[] chars = str.toCharArray ();

        for (int i = 0; i < chars.length; i++)
        {
            if (chars[i] == '\n')
            {
                buff.append ("%0A");
            }
            else if (chars[i] == '\r')
            {
                buff.append ("%0D");
            }
            else if (chars[i] == ',')
            {
                buff.append ("%2C");
            }
            else
            {
                buff.append (chars[i]);
            }
        }

        return (buff);

    }

    /**
     * Decodes a string that has been encoded using {@link #encode}.
     * 
     * @param str The String to decode.
     * @return The decoded String.
     */
    public static String decode (String str)
    {
        if (str == null)
        {
            return (str);

        }

        if (str.length () < 3)
        {
            return (str);
        }

        // Do a quick check to see if there is any
        // characters to decode
        if (str.indexOf ('%') == -1)
        {
            return (str);
        }

        StringBuffer buff = new StringBuffer ();
        char[] chars = str.toCharArray ();

        for (int i = 0; i < chars.length; i++)
        {
            if (chars[i] == '%')
            {
                if (i + 2 < chars.length)
                {
                    // See if this could be a pattern
                    if (chars[i + 1] == '0' && chars[i + 2] == 'A')
                    {
                        buff.append ('\n');
                        i += 2;
                        continue;
                    }
                    else if (chars[i + 1] == '0' && chars[i + 2] == 'D')
                    {
                        buff.append ('\r');
                        i += 2;
                        continue;
                    }
                    else if (chars[i + 1] == '2' && chars[i + 2] == 'C')
                    {
                        buff.append (',');
                        i += 2;
                        continue;
                    }
                }

            }
            buff.append (chars[i]);
        }

        return (buff.toString ());
    }

    /**
     * Gets the property value if it exists, and is parsable into an integer. If
     * the values can't be parsed into an int, or the property does not exist,
     * the default value is returned.
     * 
     * @param props The properties to use.
     * @param key The key to get the value for.
     * @param int The defaultValue to return if we can't parse the value as an
     *        int or it does not exist.
     */
    public static int getInt (Properties props, String key, int defaultValue)
    {
        if (props == null)
        {
            return (defaultValue);
        }

        String tmp = props.getProperty (key);

        if (tmp == null)
        {
            return (defaultValue);

        }

        int result = defaultValue;

        try
        {
            result = Integer.parseInt (tmp);
        }
        catch (NumberFormatException nfe)
        {
            // Fail silently just return the default
        }

        return (result);

    }

    /**
     * Converts a Throwable into a string. The string contains the message from
     * the throwable, and its stack trace. Each line in the return string is
     * separated by a "\n".
     * 
     * @param t The Throwable to convert.
     */
    public static String formatThrowable (Throwable t)
    {
        StringBuffer buff = new StringBuffer ();
        return (formatThrowable (buff, t, null, null, DEFAULT_STACK_DEPTH).toString ());

    }

    /**
     * Converts a Throwable into a string, and places it in the StringBuffer.
     * The StringBuffer will contain the message from the throwable, and its
     * stack trace. Each line added to the StringBuffer will be separated by a
     * "\n".
     * 
     * @param buff The StringBuffer to put the results in.
     * @param t The throwable to convert to a String.
     * @return The StringBuffer with the results added.
     */
    public static StringBuffer formatThrowable (StringBuffer buff, Throwable t)
    {
        return (formatThrowable (buff, t, null, null, DEFAULT_STACK_DEPTH));
    }

    /**
     * Converts a Throwable into a string, and returns it. The string contains
     * the message from the throwable, and its stack trace. Each line in the
     * return string is separated by a "\n".
     * 
     * @param t The throwable to convert to a String.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @return The String.
     */
    public static String formatThrowable (Throwable t, Class loggerClass)
    {
        StringBuffer buff = new StringBuffer ();
        return (formatThrowable (buff, t, loggerClass, null, DEFAULT_STACK_DEPTH).toString ());

    }

    /**
     * Converts a Throwable into a string, and puts it in the StringBuffer. The
     * StringBuffer will contain the message from the throwable, and its stack
     * trace. Each line added to the StringBuffer will be separated by a "\n".
     * 
     * @param buff The StringBuffer to put the results in.
     * @param t The throwable to convert to a String.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @return The result StringBuffer.
     */
    public static StringBuffer formatThrowable (StringBuffer buff, Throwable t, Class loggerClass)
    {
        return (formatThrowable (buff, t, loggerClass, null, DEFAULT_STACK_DEPTH));

    }

    /**
     * Converts a Throwable into a string, and returns it.
     * 
     * @param t The throwable to convert to a String. The string contains the
     *            message from the throwable, and its stack trace. Each line in
     *            the return string is separated by the provided line separator.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @param lineSeparator The line separator to use.
     * @return The String.
     */
    public static String formatThrowable (Throwable t, Class loggerClass, String lineSeparator)
    {
        StringBuffer buff = new StringBuffer ();
        return (formatThrowable (buff, t, loggerClass, lineSeparator, DEFAULT_STACK_DEPTH).toString ());

    }

    /**
     * Converts a Throwable into a string, and puts it in the StringBuffer. The
     * StringBuffer will contain the message from the throwable, and its stack
     * trace. Each line added to the StringBuffer will be separated by the
     * provided line separator.
     * 
     * @param buff The StringBuffer to put the results in.
     * @param t The throwable to convert to a String.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @param lineSeparator The line separator to use.
     * @return The result StringBuffer.
     */
    public static StringBuffer formatThrowable (StringBuffer buff, Throwable t, Class loggerClass, String lineSeparator)
    {
        return (formatThrowable (buff, t, loggerClass, lineSeparator, DEFAULT_STACK_DEPTH));

    }

    /**
     * Converts a Throwable into a string. The string contains the message from
     * the throwable, and its stack trace.
     * 
     * @param t The throwable to convert to a String.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @param lineSeparator The line separator to use.
     * @param maxStackDepth The maximum number of methods on the stack to return
     *            in the the result. This excludes any methods found at the top
     *            from loggerClass.
     * @return The result String.
     */
    public static String formatThrowable (Throwable t, Class loggerClass, String lineSeparator, int maxStackDepth)
    {
        StringBuffer buff = new StringBuffer ();
        return (formatThrowable (buff, t, loggerClass, lineSeparator, maxStackDepth).toString ());
    }

    /**
     * Converts a Throwable into a string, and puts it in the StringBuffer. The
     * StringBuffer will contain the message from the throwable, and its stack
     * trace.
     * 
     * @param buff The StringBuffer to put the results in.
     * @param t The throwable to convert to a String.
     * @param loggerClass Any methods on the top of the stack, which came from
     *            this class, will be removed from the output. Can be null.
     * @param lineSeparator The line separator to use.
     * @param maxStackDepth The maximum number of methods on the stack to return
     *            in the the result. This excludes any methods found at the top
     *            from loggerClass.
     * @return The result StringBuffer.
     */
    public static StringBuffer formatThrowable (StringBuffer buff, Throwable t, Class loggerClass,
                                                String lineSeparator, int maxStackDepth)
    {
        if (t == null)
        {
            return (buff);
        }

        if (maxStackDepth <= 0)
        {
            return (buff);
        }

        if (lineSeparator == null)
        {
            lineSeparator = "\n";
        }

        List exceptionLocations = FileLocation.getLocation (t);

        // Include the name of the exception class in the results
        buff.append (t.getClass ().getName ()).append (": ").append (t.getMessage ());

        // Get rid of any items on the top of the stack that match the
        // loggerClassName class.
        int i = 0;
        int size = exceptionLocations.size ();
        if (loggerClass != null)
        {
            String loggerClassName = loggerClass.getName ();
            while (i < size)
            {
                FileLocation floc = (FileLocation) exceptionLocations.get (i);
                String className = floc.getClassName ();

                if (!loggerClassName.equalsIgnoreCase (className))
                {
                    break;
                }

                ++i;
            }
        }

        if (i < size)
        {
            // Loop thru the remaining results, upto a maximum depth
            for (int j = i; j < maxStackDepth + i && j < size; j++)
            {
                FileLocation loc = (FileLocation) exceptionLocations.get (j);
                buff.append (lineSeparator).append ("at ").append (loc.getClassName ()).append ('.')
                    .append (loc.getMethodName ()).append (" (").append (loc.getFileName ()).append (':')
                    .append (loc.getLineNum ());

                // Add revision number if exists
                int rev = loc.getRevisionNum ();
                if (rev >= 0)
                {
                    buff.append (":v").append (rev);
                }

                buff.append (')');
            }
        }

        // If there is a wrapped exception, get it too
        Throwable wrapped = null;

        if (t instanceof MSException)
        {
            wrapped = ((MSException) t).getOrigException ();
        }
        else if (t instanceof SQLException)
        {
            wrapped = ((SQLException) t).getNextException ();
        }
        else
        {
            // Java 1.4 now has a standard way of chaining exceptions
            wrapped = t.getCause ();
        }

        if (wrapped != null)
        {
            buff.append (lineSeparator).append ("due to:").append (lineSeparator);
            formatThrowable (buff, wrapped, loggerClass, lineSeparator, maxStackDepth);
        }

        return (buff);
    }

    /**
     * Folds the string at the desired width, inserting &lt;br/&gt; as the line
     * separator, then replaces any tabs with 4 non-breaking spaces (&lt;nbsp;).
     * 
     * @param input The string to format.
     * @param maxWidth The maximum width to fold the string at.
     * @return The formatted string, or an empty string if input is null.
     * @throws IllegalArgumentException if maxWidth is not positive.
     */
    public static String formatForHtml (String input, int maxWidth)
    {
        if (input == null)
        {
            return ("");
        }

        // Fold the string at the desired width, using <br/> as the line
        // separator.
        input = StringUtil.fold (input, maxWidth, "<br/>");

        // replace any tabs with 4 non-breaking spaces
        return (StringUtil.replace (input, "\t", "&nbsp;&nbsp;&nbsp;&nbsp;"));
    }

    /**
     * Gets the offset that would need to be added to the UTC date to get the
     * current date in the local time zone.
     * 
     * @param date The date to find the offset for
     * @return The offset
     */
    public static int getGMTOffset (java.util.Date dt)
    {
        synchronized (s_cal)
        {
            s_cal.setTime (dt);
            return (s_cal.get (Calendar.ZONE_OFFSET) + s_cal.get (Calendar.DST_OFFSET));
        }
    }

    /**
     * Redirects stdout and stderr to a file
     * 
     * @param file The file to redirect to.
     * @throws IOException If there is any problems opening the file for
     *             writing.
     */
    public static void redirectStdOutErr (String file) throws IOException
    {
        PrintStream s = new PrintStream (new BufferedOutputStream (new FileOutputStream (file)));

        System.setOut (s);
        System.setErr (s);
    }
}
